package com.alogic.remote.httpclient;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.nio.charset.Charset;

import com.alogic.remote.util.RemoteCallMetrics;
import com.alogic.tracer.Tool;
import com.alogic.tracer.TraceContext;
import com.anysoft.rrm.RRData;
import com.anysoft.rrm.RRDataHandlerFactory;
import org.apache.http.*;
import org.apache.http.client.entity.GzipCompressingEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.entity.*;
import org.apache.http.impl.client.CloseableHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;

import com.alogic.remote.Request;
import com.alogic.remote.Response;
import com.alogic.remote.backend.Backend;
import com.alogic.rpc.CallException;
import com.anysoft.util.Properties;
import com.anysoft.stream.Handler;

/**
 * Request
 * 
 * @author yyduan
 * @since 1.6.8.12
 * 
 * @version 1.6.8.14 <br>
 * - 优化http远程调用的超时机制 <br>
 * 
 * @version 1.6.8.15 [20170511 duanyy] <br>
 * - 增加绝对路径调用功能 <br>
 * 
 * @version 1.6.10.1 [20170910 duanyy] <br>
 * - 修正httpclient连接的“failed to respond”异常;
 * 
 * @version 1.6.10.6 [20171114 duanyy] <br>
 * - 增加Filter的事件触发 <br>
 * 
 * @version 1.6.10.9 [20171124 duanyy] <br>
 * - 增加{@link #getPathInfo()}和{@link #getQueryInfo()}方法 <br>
 * 
 * @version 1.6.11.14 [duanyy 20180129] <br>
 * - 修正QueryInfo和服务器取法不一致的问题 <br>
 *
 * @version 1.6.13.6 [20200629 duanyy] <br>
 * - 增加rpc的debug信息的输出； <br>
 *
 * @version 1.6.13.21 [20201021 duanyy] <br>
 * - 优化代码结构 <br>
 * - 支持gzip压缩 <br>
 */
public class HttpClientRequest implements Request{
	protected static final Logger LOG = LoggerFactory.getLogger(HttpClientRequest.class);
	protected HttpRequestBase httpRequest = null;
	protected CloseableHttpClient httpClient = null;
	protected HttpClient client = null;
	protected HttpClientConfig clientConfig = null;
	protected boolean debug = false;
	
	public HttpClientRequest(String method,CloseableHttpClient httpClient,HttpClient client,HttpClientConfig clientConfig,boolean debug){
		this.httpClient = httpClient;
		this.client = client;
		this.clientConfig = clientConfig;
		this.debug = clientConfig.isDebug() | debug;

		httpRequest = getRequestByMethod(method);
		if (httpRequest == null){
			httpRequest = new HttpPost();
		}
		httpRequest.setConfig(clientConfig.getRequestConfig());
	}

	public HttpRequestBase getRequestByMethod(final String method){
		HttpClient.Method m = HttpClient.Method.valueOf(method.toUpperCase());
		return m.createRequest();
	}

	/**
	 * 获取当前的URI
	 * @return URI
	 */
	public String getURI(){
		URI uri = this.httpRequest != null ? this.httpRequest.getURI() : null;
		return uri != null ? uri.toString() : "";
	}
	
	public String getPathInfo(){
		URI uri = this.httpRequest != null ? this.httpRequest.getURI() : null;
		return uri != null ? uri.getPath() : "";
	}
	
	public String getQueryInfo(){
		URI uri = this.httpRequest != null ? this.httpRequest.getURI() : null;
		return uri != null ? uri.getRawQuery() : "";
	}
	
	/**
	 * 获取指定的header值
	 * @param name header名称
	 * @param dft 缺省值
	 * @return header值
	 */
	public String getHeader(String name,String dft){
		Header header = this.httpRequest != null ? this.httpRequest.getFirstHeader(name) : null;
		return header == null ? dft: header.getValue();
	}
	
	@Override
	public Request setHeader(String name, String value) {
		if (this.httpRequest != null){
			this.httpRequest.setHeader(name, value);
		}
		return this;
	}

	@Override
	public Request setBody(String text) {
		if (httpRequest != null && httpRequest instanceof HttpEntityEnclosingRequestBase){
			HttpEntityEnclosingRequestBase entityRequest = (HttpEntityEnclosingRequestBase) httpRequest;
			HttpEntity entity = new StringEntity(text,clientConfig.getEncoding());
			if (clientConfig.isGzipEnable()){
				entity = new GzipCompressingEntity(entity);
			}
			entityRequest.setEntity(entity);
		}
		return this;
	}

	@Override
	public Request setBody(byte[] body) {
		if (httpRequest != null && httpRequest instanceof HttpEntityEnclosingRequestBase){
			HttpEntityEnclosingRequestBase entityRequest = (HttpEntityEnclosingRequestBase) httpRequest;
			HttpEntity entity = new ByteArrayEntity(body);
			if (clientConfig.isGzipEnable()){
				entity = new GzipCompressingEntity(entity);
			}
			entityRequest.setEntity(entity);
		}
		
		return this;
	}

	@Override
	public Request setBody(InputStream in) {
		if (httpRequest != null && httpRequest instanceof HttpEntityEnclosingRequestBase){
			HttpEntityEnclosingRequestBase entityRequest = (HttpEntityEnclosingRequestBase) httpRequest;
			HttpEntity entity = new InputStreamEntity(in);
			if (clientConfig.isGzipEnable()){
				entity = new GzipCompressingEntity(entity);
			}
			entityRequest.setEntity(entity);
		}
		return this;
	}

	@Override
	public Request setMultipartBody(String formKey, String filename, String content) {
		if (httpRequest != null && httpRequest instanceof HttpEntityEnclosingRequestBase){
			HttpEntityEnclosingRequestBase entityRequest = (HttpEntityEnclosingRequestBase) httpRequest;
			MultipartEntityBuilder builder = MultipartEntityBuilder.create();
			builder.setCharset(Charset.forName(clientConfig.getEncoding()));
			builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
			builder.addBinaryBody(formKey, content.getBytes(), ContentType.MULTIPART_FORM_DATA, filename);
			entityRequest.setEntity(builder.build());
		}
		return this;
	}

	@Override
	public Request setMultipartBody(String formKey, String filename, byte[] content) {
		if (httpRequest != null && httpRequest instanceof HttpEntityEnclosingRequestBase){
			HttpEntityEnclosingRequestBase entityRequest = (HttpEntityEnclosingRequestBase) httpRequest;
			MultipartEntityBuilder builder = MultipartEntityBuilder.create();
			builder.setCharset(Charset.forName(clientConfig.getEncoding()));
			builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
			builder.addBinaryBody(formKey,content, ContentType.MULTIPART_FORM_DATA, filename);
			entityRequest.setEntity(builder.build());
		}
		return this;
	}

	@Override
	public Request setMultipartBody(String formKey, String filename, InputStream in) {
		if (httpRequest != null && httpRequest instanceof HttpEntityEnclosingRequestBase){
			HttpEntityEnclosingRequestBase entityRequest = (HttpEntityEnclosingRequestBase) httpRequest;
			MultipartEntityBuilder builder = MultipartEntityBuilder.create();
			builder.setCharset(Charset.forName(clientConfig.getEncoding()));
			builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
			builder.addBinaryBody(formKey, in, ContentType.MULTIPART_FORM_DATA, filename);
			entityRequest.setEntity(builder.build());
		}
		return this;
	}

	@Override
	public Request setBody(DirectOutput out){
		if (httpRequest != null && httpRequest instanceof HttpEntityEnclosingRequestBase){
			HttpEntityEnclosingRequestBase entityRequest = (HttpEntityEnclosingRequestBase) httpRequest;
			HttpEntity entity = new DirectOutputEntity(out);
			if (clientConfig.isGzipEnable()){
				entity = new GzipCompressingEntity(entity);
			}
			entityRequest.setEntity(entity);
		}
		
		return this;
	}
	
	@Override
	public Response execute(String path,String key,Properties ctx) {
		int retryCount = 0;
		int autoRetry = 0;
		Response result = null;
		String lastErrorCode = "core.io_error";
		String lastErrorMsg = "";
		
		while (true){
			Backend backend = null;
			long start = System.nanoTime();
			boolean error = false;
			try {
				backend = client.getBackend(key,ctx, autoRetry > 0 ? 0 : retryCount ++ );				
				if (backend != null){
					result = execute(path,backend,path);
				}
				break;
			}catch (CallException ex){
				error = true;
				lastErrorCode = ex.getCode();
				lastErrorMsg = ex.getMessage();
				if (lastErrorCode.equals("core.e1602") 
						|| lastErrorCode.equals("core.e1603")
						|| lastErrorCode.equals("core.e1604")){
					//对于internal错误，属于连接错误，可以重试
					autoRetry = autoRetry >= clientConfig.getAutoRetryCnt() ? 0 : autoRetry + 1;
					if (autoRetry > 0){
						LOG.error("Internal error occurs,Retry " + autoRetry);
					}
				}else{
					if (!lastErrorCode.startsWith("core")){
						throw ex;
					}
				}
			}finally{
				if (backend != null){
					backend.count(System.nanoTime() - start, error);
				}
			}
		}
		
		if (result == null){
			throw new CallException(lastErrorCode,lastErrorMsg);
		}
		return result;
	}
	

	@Override
	public Response execute(String fullPath) {
		String url = fullPath;
		long start = System.nanoTime();
		boolean error = false;
		String reason = "ok";
		TraceContext tc = null;
		if (clientConfig.isTraceEnable()){
			tc = Tool.start();
		}
		try {			
			httpRequest.setURI(URI.create(url));
			//request事件
			client.onRequest(this);
			TraceContext child = tc == null ? null:tc.newChild();
			if (child != null){
				httpRequest.setHeader(clientConfig.getTraceId(), child.sn());
				httpRequest.setHeader(clientConfig.getTraceOrder(), child.order());
			}
			if (debug){
				LOG.info(String.format("Request:%s",fullPath));
				LOG.info("\tHeaders:");
				HeaderIterator iter = httpRequest.headerIterator();
				while (iter.hasNext()){
					Header header = iter.nextHeader();
					LOG.info(String.format("\t\t%s:%s",header.getName(),header.getValue()));
				}
			}
			CloseableHttpResponse resp = httpClient.execute(httpRequest);
			if (debug){
				StatusLine status = resp.getStatusLine();
				LOG.info(String.format("Response:%s-%s",status.getStatusCode(),status.getReasonPhrase()));
				LOG.info("\tHeaders:");
				HeaderIterator iter = resp.headerIterator();
				while (iter.hasNext()){
					Header header = iter.nextHeader();
					LOG.info(String.format("\t\t%s:%s",header.getName(),header.getValue()));
				}
			}
			HttpClientResponse response =  new HttpClientResponse(resp,clientConfig.getEncoding());
			//response事件
			client.onResponse(response);
			return response;
		}catch (SocketTimeoutException ex){
			error = true;
			reason = ex.getMessage();
			throw new CallException("core.e1601",url, ex);
		}catch (ConnectTimeoutException ex){
			error = true;
			reason = ex.getMessage();
			throw new CallException("core.e1602",url, ex);
		}catch (ConnectException ex){
			error = true;
			reason = ex.getMessage();
			throw new CallException("core.e1603",url, ex);
		}catch (NoHttpResponseException ex){
			error = true;
			reason = ex.getMessage();
			throw new CallException("core.e1604",url, ex);
		}catch (Exception ex){
			error = true;
			reason = ex.getMessage();
			throw new CallException("core.e1004",url, ex);
		}finally{
			if (clientConfig.isMetricsOutput()){
				RemoteCallMetrics m = new RemoteCallMetrics(url);
				m.count(System.nanoTime() - start,error);
                Handler<RRData> handler = RRDataHandlerFactory.getHandler();
                if (handler != null){
                    handler.handle(m,System.currentTimeMillis());
                }
			}
			if (clientConfig.isTraceEnable()){
				Tool.end(tc, "Remote", url, error?"FAILED":"OK", reason);
			}
		}
	}
	
	@Override
	public Response execute(String fullPath,String rpcId) {
		String url = fullPath;
		long start = System.nanoTime();
		boolean error = false;
		String reason = "ok";
		TraceContext tc = null;
		if (clientConfig.isTraceEnable()){
			tc = Tool.start();
		}
		try {			
			httpRequest.setURI(URI.create(url));
			//request事件
			client.onRequest(this);
			TraceContext child = tc == null ? null:tc.newChild();
			if (child != null){
				httpRequest.setHeader(clientConfig.getTraceId(), child.sn());
				httpRequest.setHeader(clientConfig.getTraceOrder(), child.order());
			}
			if (debug){
				LOG.info(String.format("Request:%s",fullPath));
				LOG.info("\tHeaders:");
				HeaderIterator iter = httpRequest.headerIterator();
				while (iter.hasNext()){
					Header header = iter.nextHeader();
					LOG.info(String.format("\t\t%s:%s",header.getName(),header.getValue()));
				}
			}
			CloseableHttpResponse resp = httpClient.execute(httpRequest);
			if (debug){
				StatusLine status = resp.getStatusLine();
				LOG.info(String.format("Response:%s-%s",status.getStatusCode(),status.getReasonPhrase()));
				LOG.info("\tHeaders:");
				HeaderIterator iter = resp.headerIterator();
				while (iter.hasNext()){
					Header header = iter.nextHeader();
					LOG.info(String.format("\t\t%s:%s",header.getName(),header.getValue()));
				}
			}
			HttpClientResponse response =  new HttpClientResponse(resp,clientConfig.getEncoding());
			//response事件
			client.onResponse(response);
			return response;
		}catch (SocketTimeoutException ex){
			error = true;
			reason = ex.getMessage();
			throw new CallException("core.e1601",url, ex);
		}catch (ConnectTimeoutException ex){
			error = true;
			reason = ex.getMessage();
			throw new CallException("core.e1602",url, ex);
		}catch (ConnectException ex){
			error = true;
			reason = ex.getMessage();
			throw new CallException("core.e1603",url, ex);
		}catch (NoHttpResponseException ex){
			error = true;
			reason = ex.getMessage();
			throw new CallException("core.e1604",url, ex);
		}catch (Exception ex){
			error = true;
			reason = ex.getMessage();
			ex.printStackTrace();
			throw new CallException("core.e1004",url, ex);
		}finally{
			if (clientConfig.isMetricsOutput()){
				RemoteCallMetrics m = new RemoteCallMetrics(url,rpcId);
				m.count(System.nanoTime() - start,error);
                Handler<RRData> handler = RRDataHandlerFactory.getHandler();
                if (handler != null){
                    handler.handle(m,System.currentTimeMillis());
                }
			}
			if (clientConfig.isTraceEnable()){
				Tool.end(tc, "Remote", url, error?"FAILED":"OK", reason);
			}
		}
	}

	public String getMetricsId(String id){
		return String.format("rpc.thpt:%s",id);
	}

	protected Response execute(String path,Backend backend,String rpcId) {
		String url = client.getInvokeURL(backend, path);
		long start = System.nanoTime();
		boolean error = false;
		String reason = "ok";
		TraceContext tc = null;
		if (clientConfig.isTraceEnable()){
			tc = Tool.start();
		}
		try {		
			httpRequest.setURI(URI.create(url));
			//request事件
			client.onRequest(this);
			TraceContext child = tc == null ? null:tc.newChild();
			if (child != null){
				httpRequest.setHeader(clientConfig.getTraceId(), child.sn());
				httpRequest.setHeader(clientConfig.getTraceOrder(), child.order());
			}
			if (debug){
				LOG.info(String.format("Request:%s",url));
				LOG.info("\tHeaders:");
				HeaderIterator iter = httpRequest.headerIterator();
				while (iter.hasNext()){
					Header header = iter.nextHeader();
					LOG.info(String.format("\t\t%s:%s",header.getName(),header.getValue()));
				}
			}
			CloseableHttpResponse resp = httpClient.execute(httpRequest);
			if (debug){
				StatusLine status = resp.getStatusLine();
				LOG.info(String.format("Response:%s-%s",status.getStatusCode(),status.getReasonPhrase()));
				LOG.info("\tHeaders:");
				HeaderIterator iter = resp.headerIterator();
				while (iter.hasNext()){
					Header header = iter.nextHeader();
					LOG.info(String.format("\t\t%s:%s",header.getName(),header.getValue()));
				}
			}
			HttpClientResponse response =  new HttpClientResponse(resp,clientConfig.getEncoding());
			//response事件
			client.onResponse(response);			
			return response;
		}catch (SocketTimeoutException ex){
			error = true;
			reason = ex.getMessage();
			throw new CallException("core.e1601",url, ex);
		}catch (ConnectTimeoutException ex){
			error = true;
			reason = ex.getMessage();
			throw new CallException("core.e1602",url, ex);
		}catch (ConnectException ex){
			error = true;
			reason = ex.getMessage();
			throw new CallException("core.e1603",url, ex);
		}catch (NoHttpResponseException ex){
			error = true;
			reason = ex.getMessage();
			throw new CallException("core.e1604",url, ex);
		}catch (Exception ex){
			error = true;
			reason = ex.getMessage();
			throw new CallException("core.e1004",url, ex);
		}finally{
			if (clientConfig.isMetricsOutput()){
                RemoteCallMetrics m = new RemoteCallMetrics(url,rpcId);
                m.count(System.nanoTime() - start,error);
                Handler<RRData> handler = RRDataHandlerFactory.getHandler();
                if (handler != null){
                    handler.handle(m,System.currentTimeMillis());
                }
			}
			if (clientConfig.isTraceEnable()){
				Tool.end(tc, "Remote", url, error?"FAILED":"OK", reason);
			}
		}
	}

	@Override
	public void close() {
		if (httpRequest != null){
			httpRequest.releaseConnection();
		}
	}

	public static class DirectOutputEntity extends AbstractHttpEntity{
		protected DirectOutput out = null;
		
		public DirectOutputEntity(DirectOutput out){
			this.out = out;
		}
		
		@Override
		public boolean isRepeatable() {
			return true;
		}

		@Override
		public long getContentLength() {
			// i don't know.
			return -1;
		}

		@Override
		public InputStream getContent() throws IOException,
				UnsupportedOperationException {
			throw new UnsupportedOperationException();
		}

		@Override
		public void writeTo(OutputStream outstream) throws IOException {
			if (this.out != null){
				this.out.writeTo(outstream);
			}
		}

		@Override
		public boolean isStreaming() {
			return false;
		}
		
	}

}
